64 — 有趣又简单的 R3F 作品集
介绍
如果你看到这里,并且正试图用 Three.js 提升自己的 WebGL 技能,那你大概知道拥有一个优秀的作品集有多么重要。
在网页开发中,更广泛地说在数字行业中,最重要的是你的经验。公司和招聘人员在给你机会之前,肯定会首先根据这一标准来评判你。
你需要一种方式来展示你的项目,因此拥有一个作品集很重要。
而这个作品集本身其实就是你的项目之一(如果不是最重要的那个的话)。它应该成为你的见证,展示你能做什么,以及公司为什么要聘用你。
但创建一个酷炫的作品集是一个漫长而艰难的过程。你是你自己的客户,而且你很可能是你能遇到的最苛刻的客户。
不过,假设你已经有了一个更简单的 HTML/CSS 作品集,用来展示你的项目和一些信息(这已经很不错了)。
在这节课中,我们将在这个 HTML/CSS 作品集的基础上加以利用,让它看起来更令人兴奋、更有创意。我们将创建一个漂亮的 3D 场景,里面有一台笔记本电脑模型,并把那个 HTML/CSS 作品集放进笔记本电脑的屏幕里。
这个概念的灵感来自 Paul Henschel(0xca0a)的实验,你可以在这里找到它 https://twitter.com/0xca0a/status/1398633764931178498
请注意,在整个课程中,我们会设置一些恰到好处的值,让体验令人愉悦。为了得到这些值,我不得不反复调整,并且我使用 Leva 来完成。
如果你正在创建自己的体验,不要因为没能第一次就找到完美的值而感到沮丧。这是一个反复试验的过程。别忘了添加一个像 Leva 这样的调试 UI 来帮助你。
初始设置
在起始项目里,我们在中心有一个简单的立方体。
@react-three/drei 依赖已经安装在项目中了,我们使用 OrbitControls 辅助工具来绕相机移动。我们稍后会替换它,但现在先保留它。
我们没有使用 Perf,因为我们不应该有任何性能方面的顾虑,但如果你愿意,可以随意添加它。
我们没有使用 Leva,因为我们会设置非常具体的值,但如果你想调整体验,应该添加它。
背景
首先,让我们把背景改成更深的颜色,以营造更温暖的氛围,并突出内容的重要性。
在 Experience.jsx 中,添加一个带有 attach 属性并设为 "background" 的 <color>:
export default function Experience()
{
return <>
<color args={ [ '#241a1a' ] } attach="background" />
{/* ... */}
</>
}
笔记本电脑
我们将把作品集显示在一台笔记本电脑上。
我们可以自己创建这个模型,但不如使用一个免费的模型。
找一个好模型
网上有很多免费的模型,但 PMNDRS 团队创建了一个地方,我们可以在那里找到一系列可以直接在 Three.js 中、更具体地说是在 R3F 中使用的模型:https://market.pmnd.rs
如果你在找 "Macbook" 模型,你应该能找到这个 https://market.pmnd.rs/model/macbook。
大多数模型都在 CC0 许可证(“保留所有权利”的反面)下提供,这意味着你可以对它们做任何你想做的事。
顺便说一下,如果你想帮忙,可以向 PMNDRS Market 提交你自己的模型。
实现模型
我们有多种选择可以把模型引入我们的体验:
- 下载它并放进
/public/文件夹 - 使用来自 https://market.pmnd.rs/ 的 CDN 链接
- 下载各种 R3F 版本
- 下载原生 Three.js 版本
让我们保持简单,使用 CDN。
点击“copy direct link”。
在 Experience.jsx 中,从 @react-three/drei 导入 useGLTF:
import { useGLTF, OrbitControls } from '@react-three/drei' 然后用 useGLTF 加载模型(使用你从 PMNDRS Market 复制的链接,而不是下面这个,即使它们是同一个模型):
export default function Experience()
{
const computer = useGLTF('https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/macbook/model.gltf')
// ...
} 如果由于某些原因 https://market.pmnd.rs 无法访问,你可以使用以下链接:
'https://threejs-journey.com/resources/models/macbook_model.gltf' 最后,用 <primitive> 及其 object 属性添加它:
export default function Experience()
{
// ...
return <>
{/* ... */}
<primitive object={ computer.scene } />
</>
}
你应该会看到一台全黑的笔记本电脑,因为我们需要灯光。
灯光与环境
我们将使用环境光照,并且通过使用来自 drei 的 Environment 辅助工具并搭配一个预设,超级偷懒一番。
从 @react-three/drei 导入 Environment:
import { Environment, useGLTF, OrbitControls } from '@react-three/drei' 把它添加到场景中的任意位置,并将 "city" 作为 preset 属性:
export default function Experience()
{
// ...
return <>
<Environment preset="city" />
{/* ... */}
</>
}
现在我们可以借助合适的光源来欣赏这个模型了。
既然我们终于能看见东西了,那就把立方体移除:
漂浮动画
我们将让笔记本电脑缓慢移动,但首先,让我们把它放到场景的正中央。
我们本可以使用 <Center> 辅助工具,但那样会导致整台电脑被居中,其屏幕会有点偏。
相反,我们将把 position-y 设为 - 1.2:
<primitive
object={ computer.scene }
position-y={ - 1.2 }
/>
现在我们可以让笔记本电脑漂浮起来了,我们将使用 drei 辅助工具中的 Float。
首先,从 @react-three/drei 导入 Float:
import { Float, Environment, useGLTF, OrbitControls } from '@react-three/drei' 然后,把 <primitive> 包在 <Float> 里:
<Float>
<primitive
object={ computer.scene }
position-y={ - 1.2 }
/>
</Float>
默认的漂浮动画感觉不错,但请记住我们要在屏幕上显示一个 HTML/CSS 网站。如果屏幕移动得太多,你的网站访客就无法与网站交互了。
用 rotationIntensity 属性降低旋转强度:
<Float rotationIntensity={ 0.4 } >
<primitive
object={ computer.scene }
position-y={ - 1.2 }
/>
</Float>
相机与控制
目前,我们使用的是来自 drei 的 OrbitControls,我们可以绕模型全方位旋转。
这没什么大不了的,但最好是限制旋转,同时仍然允许用户稍微旋转一下。
与其调整 OrbitControls 的属性,我们将使用一个名为 PresentationControls 的 drei 辅助工具。
PresentationControls 让你操作的是模型而不是相机。我们可以旋转那个模型,而当我们松开它时,它会回到初始位置。
在内部,它使用了 Spring;这是另一个 PMNDRS 库,它让动画变得真实而令人愉悦。
实现
从 @react-three/drei 导入 PresentationControls:
import { PresentationControls, Float, Environment, useGLTF, OrbitControls } from '@react-three/drei' 现在移除 <OrbitControls> 及其导入,因为我们不希望两个不同的控制器相互干扰。
最后,把 <PresentationControls> 添加到 <Float> 周围:
export default function Experience()
{
// ...
return <>
<color args={ [ '#241a1a' ] } attach="background" />
<Environment preset="city" />
<PresentationControls>
<Float rotationIntensity={ 0.4 } >
<primitive
object={ computer.scene }
position-y={ - 1.2 }
/>
</Float>
</PresentationControls>
</>
}
默认行为感觉很像 OrbitControls,但我们可以对它加以改进。
全局
默认情况下,我们只能通过在模型上直接拖放来旋转物体。
我们希望能够从任何地方拖放,甚至是背景。
要做到这一点,添加 global 属性:
<PresentationControls
global
>
{/* ... */}
</PresentationControls>
修复移动端手势错误
如果你现在拖放物体,你可能会注意到控制台中出现一个警告:
即使你没有看到这个错误,下面的问题也是存在的。
PresentationControls 使用了 use-gesture,这是一个使用自然手势(比如拖放)与元素交互的库,该库建议把 CSS 属性 touch-action 设为 none,以修复移动端在滑动时的怪异行为。
确实,如果你在移动端测试当前的体验,你会注意到拖放有点笨拙,并且会与浏览器默认的手势冲突,比如从上到下拖放时重新加载页面。
你可以在这里找到关于此问题的更多信息:https://use-gesture.netlify.app/docs/extras/#touch-action
按照那个网站上所说的,我们需要在 CSS 中添加 touch-action 属性,但需要在处理指针事件的元素上添加,也就是 R3F 创建的、正好位于 <canvas> 上方的那个 <div>。
默认情况下,这个 <div> 没有 class,但我们可以通过在 <Canvas> 上使用 className 来设置一个:
<Canvas
className="r3f"
camera={ {
fov: 45,
near: 0.1,
far: 2000,
position: [ -3, 1.5, 4 ]
} }
>
<Experience />
</Canvas> 我们选择了 "r3f",但你可以改成任何你想要的。
接下来,在 /src/style.css 中,我们可以选中它并把它的 touch-action 改为 none:
.r3f
{
touch-action: none;
}
旋转
我们可以用 rotation 属性更改默认旋转:
<PresentationControls
global
rotation={ [ 0.13, 0.1, 0 ] }
>
{/* ... */}
</PresentationControls>
限制
我们可以同时限制水平和垂直方向的旋转。
垂直方向的那个叫做 polar,我们必须传入一个包含两个值的数组,即最小值和最大值:
<PresentationControls
global
rotation={ [ 0.13, 0.1, 0 ] }
polar={ [ - 0.4, 0.2 ] }
>
{/* ... */}
</PresentationControls>
水平方向的那个叫做 azimuth,用法相同:
<PresentationControls
global
rotation={ [ 0.13, 0.1, 0 ] }
polar={ [ - 0.4, 0.2 ] }
azimuth={ [ - 1, 0.75 ] }
>
{/* ... */}
</PresentationControls>
Spring 配置
最后,我们想调整动画的物理效果。
正如我们之前所说,PresentationControls 使用了 Spring 来做动画,我们可以调整那个 spring 的属性。
我们不打算详细讲解 spring 的所有属性,但你可以在这里查看文档 https://react-spring.dev/common/configs#configs
有两种不同的 spring 配置。
拖放时使用的那一个可以用 config 属性设置:
<PresentationControls
global
rotation={ [ 0.13, 0.1, 0 ] }
polar={ [ - 0.4, 0.2 ] }
azimuth={ [ - 1, 0.75 ] }
config={ { mass: 2, tension: 400 } }
>
{/* ... */}
</PresentationControls>
松开时使用的那一个可以用 snap 属性设置:
<PresentationControls
global
rotation={ [ 0.13, 0.1, 0 ] }
polar={ [ - 0.4, 0.2 ] }
azimuth={ [ - 1, 0.75 ] }
config={ { mass: 2, tension: 400 } }
snap={ { mass: 4, tension: 400 } }
>
{/* ... */}
</PresentationControls>
阴影
为了增加场景的真实感,我们将添加一个阴影。
正如我们在之前的课程中看到的,有很多选项可供我们选择,但让我们保持简单,使用来自 drei 的 ContactShadows。
首先,从 @react-three/drei 导入 ContactShadows:
import { ContactShadows, PresentationControls, Float, Environment, useGLTF } from '@react-three/drei' 然后,把它添加到 <PresentationControls> 外面(我们不希望它旋转),并用 position-y 属性把它向下移动一点:
export default function Experience()
{
// ...
return <>
{/* ... */}
<PresentationControls
global
rotation={ [ 0.13, 0.1, 0 ] }
polar={ [ - 0.4, 0.2 ] }
azimuth={ [ - 1, 0.75 ] }
config={ { mass: 2, tension: 400 } }
snap={ { mass: 4, tension: 400 } }
>
{/* ... */}
</PresentationControls>
<ContactShadows position-y={ - 1.4 } />
</>
}
默认情况下它看起来还不错,但让我们稍微调整一下。
更改它的 opacity、scale 和 blur 属性:
<ContactShadows
position-y={ - 1.4 }
opacity={ 0.4 }
scale={ 5 }
blur={ 2.4 }
/>
iframe
是时候添加真正的 HTML/CSS 网站了。
我们希望它显示在我们的模型上,就好像它是真正的屏幕一样。
HTML/CSS 网站
首先,我们需要一个真正的 HTML/CSS 网站。
你可以使用你自己的,但作为示例,我们将使用我的作品集的一个 HTML/CSS 版本,你可以在这里找到它 https://bruno-simon.com/html/
如果你使用自己的网站,请确保服务器允许你把这些 URL 用作 iframe(如果有问题,你马上就会看到)。
实现
我们将使用一个 <iframe> 把那个网站放入我们的体验中。
如果你不知道 iframe 是什么,可以把它想象成一扇通向另一个网站的窗口。它使得网站可以在其他网站上显示(网站套娃)。
但别忘了,我们希望那个 iframe 跟随模型,就好像它是我们 WebGL 场景的一部分一样。
要做到这一点,我们将使用来自 drei 的 Html 辅助工具:
首先,从 @react-three/drei 导入 Html:
import { Html, ContactShadows, PresentationControls, Float, Environment, useGLTF } from '@react-three/drei' 现在,把它添加到 <primitive> 里面,这样 HTML 就会贴着笔记本电脑模型:
<primitive
object={ computer.scene }
position-y={ - 1.2 }
rotation-x={ 0.13 }
>
<Html>This is going to be an iframe</Html>
</primitive>
现在我们可以把文本替换成一个 <iframe>,其 src 属性指向我们的 HTML/CSS 网站:
<Html>
<iframe src="https://bruno-simon.com/html/" />
</Html>
它生效了,但我们还有一些改进要做。
首先,我们希望 iframe 能够自己进行变换,就好像它是场景的一部分,而不仅仅是跟随一个 3D 点。
变换
要做到这一点,添加 transform 属性:
<Html
transform
>
{/* ... */}
</Html>
尺寸
目前,我们看到的是一个非常小的 <iframe>,但被超级拉伸了(如果这说得通的话)。
首先,我们要确定一个尺寸,让 iframe 里的内容看起来像样。我们现在不关心它在场景中占多大,我们只希望内容能合适地放进 iframe 里。要做到这一点,我们需要对它应用一些 CSS。
让我们给它添加一个类,以便在 CSS 中控制它。
添加一个 wrapperClass 属性,内容为 htmlScreen:
<Html
transform
wrapperClass="htmlScreen"
>
{/* ... */}
</Html> 现在,iframe 的父元素上就有了一个 htmlScreen 类。
然后,在 /src/style.css 中,添加 width 和 height 属性:
.htmlScreen iframe
{
width: 1024px;
height: 670px;
}
真是一团糟!
别担心,我们现在需要做的就是把最终元素变小。
我们可以用 distanceFactor 属性做到这一点:
<Html
transform
wrapperClass="htmlScreen"
distanceFactor={ 1.17 }
>
{/* ... */}
</Html>
小修复
能够在 CSS 中控制 iframe 的外观,是一个修复小问题的好机会。
首先,我们可以移除 iframe 的默认边框:
.htmlScreen iframe
{
width: 1024px;
height: 670px;
border: none;
}
虽然如果你是 Apple 粉丝的话会觉得不准确,但我们要用 border-radius 在角落里添加一个小斜面:
.htmlScreen iframe
{
width: 1024px;
height: 670px;
border: none;
border-radius: 20px;
}
最后,我们要给它添加一个黑色背景,以防 iframe 中的 CSS 加载得不够快:
.htmlScreen iframe
{
width: 1024px;
height: 670px;
border: none;
border-radius: 20px;
background: #000000;
}
位置与旋转
现在我们可以用 position 属性把 iframe 移到模型的屏幕上了:
<Html
transform
wrapperClass="htmlScreen"
distanceFactor={ 1.17 }
position={ [ 0, 1.56, - 1.4 ] }
>
{/* ... */}
</Html>
然后,用 rotation-x 让它绕 x 轴旋转:
<Html
transform
wrapperClass="htmlScreen"
distanceFactor={ 1.17 }
position={ [ 0, 1.56, - 1.4 ] }
rotation-x={ - 0.256 }
>
{/* ... */}
</Html>
屏幕灯光
事情开始看起来真的很不错了。我们将在屏幕位置添加一个橙色的 RectAreaLight,以增加更多的真实感,这样我们就能看到光反射在笔记本电脑键盘上。
在 <Float> 里面添加一个 <rectAreaLight>,并带有以下属性:
<Float rotationIntensity={ 0.4 } >
<rectAreaLight
width={ 2.5 }
height={ 1.65 }
intensity={ 65 }
color={ '#ff6900' }
rotation={ [ - 0.1, Math.PI, 0 ] }
position={ [ 0, 0.55, - 1.15 ] }
/>
{/* ... */}
</Float>
你可以根据正在显示的页面随意更改颜色。
文本
我们快要完成了,但我们的项目对它的用途还缺乏清晰的理解。我们是在卖电脑吗?
为了让人们明显地看出这个网站代表的是你的作品集,我们将把我们的名字添加到右上角部分,并且我们将使用来自 drei 的 Text 辅助工具。
实现
从 @react-three/drei 导入 Text:
import { Text, Html, ContactShadows, PresentationControls, Float, Environment, useGLTF } from '@react-three/drei' 把它添加到 <Float> 里面,并放入你的名字或任何你想要的内容:
<Float rotationIntensity={ 0.4 } >
{/* ... */}
<Text>BRUNO SIMON</Text>
</Float>
你应该能看到文本,但要小心,iframe 位于 <canvas> 之上。
这没什么大不了的,因为我们要把它移到右上角。
字体
来自 Google Fonts 的 Bangers 字体可以在 /public/ 文件夹中找到。
它看起来有点傻气、像卡通一样,营造出一种有趣的对比。
你可以随意使用任何你喜欢的字体,并记住你可以用 Google Webfonts Helper 从 Google Fonts 下载字体。
用 font 属性设置字体,并用 fontSize 属性更改其大小:
<Text
font="./bangers-v20-latin-regular.woff"
fontSize={ 1 }
>
BRUNO SIMON
</Text>
位置与旋转
用以下 position 和 rotation 属性把它移到一边:
<Text
font="./bangers-v20-latin-regular.woff"
fontSize={ 1 }
position={ [ 2, 0.75, 0.75 ] }
rotation-y={ - 1.25 }
>
BRUNO SIMON
</Text>
换行
我们希望文本能分成两行,我们可以通过用 maxWidth 属性设置一个受限的宽度来做到这一点:
<Text
font="./bangers-v20-latin-regular.woff"
fontSize={ 1 }
position={ [ 2, 0.75, 0.75 ] }
rotation-y={ - 1.25 }
maxWidth={ 2 }
>
BRUNO SIMON
</Text>
如果你的名字长度与姓氏相差很大,你也可以调整 textAlign 属性,让文本与场景很好地契合。
更进一步
就这样!在一个小时内,我们为我们的 HTML/CSS 作品集创建了一个好看的替代方案。
请记住,这只是一个快速而简单的方案,我们还可以走得更远。
以下是一些改进的想法:
- 入场动画,让物体在准备并加载完成时出现;
- 音效(别忘了用户需要通过点击或按键与页面交互后才能播放声音);
- 悬停在屏幕上时让相机放大到屏幕(目前 UX 真的不太好,很难阅读);
- 彩蛋;
- 更好的环境,比如背景中的物体、粒子等(这里有一个不错的灵感 https://codesandbox.io/s/interactive-spline-scene-live-html-f79ucc);
- 屏幕上的反射(这个可能有点棘手,因为你需要把它们作为 HTML/CSS 内容创建在
<Html>内部,因为 iframe 位于 WebGL 之上); - 改进 iframe 的实际内容;
- 让它对移动端友好。